package com.danicsoft.daide.serverclient;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Set;

import com.danicsoft.daide.binary.Connection;
import com.danicsoft.daide.binary.ServerConnection;
import com.danicsoft.daide.binary.StateChangeHandler;
import com.danicsoft.daide.command.MapDefinitionCommand;
import com.danicsoft.daide.command.NowCommand;
import com.danicsoft.daide.command.OrderCommand;
import com.danicsoft.daide.command.SupplyOwnershipCommand;
import com.danicsoft.daide.command.OrderSpec;
import com.danicsoft.daide.command.Turn;
import com.danicsoft.daide.command.UnitPos;
import com.danicsoft.daide.historian.TokenHistorian;
import com.danicsoft.daide.token.StandardToken;
import com.danicsoft.daide.token.TokenMap;
import com.danicsoft.daide.token.TokenParser;
import com.danicsoft.daide.token.TokenMap.OrderNotes;
import com.danicsoft.daide.token.TokenMap.Orders;
import com.danicsoft.daide.token.TokenMap.Phases;

/**
 * The base class for a server. Any user wishing to implement a server must
 * create a subclass and implement the abstract methods.
 * 
 * @author Daniel Yule (daniel.yule@gmail.com)
 * @version 1.0
 */
public abstract class DaideServer implements Server, StateChangeHandler {

	/**
	 * Contains information the server tracks about a given player.
	 * 
	 * @author Daniel Yule (daniel.yule@gmail.com)
	 * @version 1.0
	 */
	private class PlayerInfo {

		/**
		 * Whether or not this player is currently connected.
		 */
		public boolean isConnected = true;

		/**
		 * Whether or not this player is currently playing.
		 */
		public boolean isPlaying = false;

		/**
		 * Whether or not this player has all their orders.
		 */
		public boolean hasAllOrders = false;

		/**
		 * Whether or not this player is ready for order processing
		 */
		public boolean isReady;

		/**
		 * The power this player represents.
		 */
		public StandardToken myPower;

		/**
		 * The player for this player info.
		 */
		public Player player;

		/**
		 * The orders that this player has submitted for this turn.
		 */
		public Set<OrderSpec> submittedOrders = new HashSet<OrderSpec>();

		/**
		 * The number of supply centres we had last winter.
		 */
		private int lastSCCount = 0;

		/**
		 * Constructs a new <code>PlayerInfo</code> class from the given
		 * <code>Player</code>
		 * 
		 * @param player
		 *        The player ths contains information for.
		 */
		public PlayerInfo(Player player) {
			this.player = player;
		}

		@Override
		public boolean equals(Object obj) {
			if (obj == null)
				return false;
			return hashCode() == obj.hashCode();
		}

		@Override
		public int hashCode() {
			return player.hashCode();
		}

		/**
		 * Whether or not this player is an observer. (True by default)
		 */
		public boolean isObserver;

		/**
		 * Look for units that have not recieved orders and informs the player
		 * of these missing units.
		 * 
		 * @param notifyPlayer
		 *        True if we are to notify the player of the missing units,
		 *        false otherwise.
		 */
		public void lookForMissingOrders(boolean notifyPlayer) {
			List<UnitPos> missingPositions = new ArrayList<UnitPos>();
			switch (thisTurn.turn.season.getData()) {
				case Phases.SPR:
				case Phases.FAL:

					for (UnitPos position : thisTurn.armyPosition.positions()) {
						if (position.power.equals(myPower) && !submittedOrders.contains(position)) {
							missingPositions.add(position);
						}
					}
					if (missingPositions.size() != 0) {
						if (notifyPlayer)
							player.notifyOfMissingOrders(missingPositions);
						hasAllOrders = false;
					}
					else {
						hasAllOrders = true;
					}
					break;
				case Phases.SUM:
				case Phases.AUT:
					for (UnitPos position : thisTurn.armyPosition.positions()) {
						if (position.power.equals(myPower) && !submittedOrders.contains(position) && position.isRetreating()) {
							missingPositions.add(position);
						}
					}
					if (missingPositions.size() != 0) {
						if (notifyPlayer)
							player.notifyOfMissingRetreats(missingPositions);
						hasAllOrders = false;
					}
					else {
						hasAllOrders = true;
					}

					break;
				case Phases.WIN:
					int removalCount = thisTurn.scOwnership.getSupplyCentreCount(myPower) - lastSCCount;
					for (OrderSpec order : submittedOrders) {
						if (order.predicate.matches(TokenMap.ORDERS, Orders.REM)) {
							removalCount--;
						}
						else if (order.predicate.matches(TokenMap.ORDERS, Orders.BLD)) {
							removalCount++;
						}
					}
					if (removalCount != 0) {
						if (notifyPlayer)
							player.notifyOfMissingRemovals(removalCount);
						hasAllOrders = false;
					}
					else {
						hasAllOrders = true;
					}

			}
		}

		/**
		 * Inform this player that the next turn is happening.
		 * 
		 * @param turn
		 *        The turn that this is.
		 */
		public void nextTurn(TurnInfo turn) {
			if (turn.turn.season.matches(TokenMap.PHASES, Phases.SPR)) {
				lastSCCount = turn.scOwnership.getSupplyCentreCount(myPower);
			}
			resetOrders();
			player.nextTurn(turn);
		}

		/**
		 * Clears the orders that have been submitted by this player.
		 */
		public void resetOrders() {
			submittedOrders = new HashSet<OrderSpec>();
			hasAllOrders = false;
		}

		/**
		 * Inform the player that we are starting the game.
		 */
		public void startGame() {
			if (isObserver) {
				player.startGame(thisTurn);
			}
			else {
				try {
					player.startGame(myPower = getUnassignedPower(), this.hashCode(), getVariantInfo(), thisTurn);
				}
				catch (IllegalServerStateException e) {
					// TODO log this craziness
				}
			}
		}
	}

	/**
	 * A singleton random instance to use to generate new random numbers.
	 */
	private static Random rand = new Random();

	/**
	 * The number of powers we need to play the game.
	 */
	private int maxPowers;

	/**
	 * The participants currently involved in this game.
	 */
	private Map<Player, PlayerInfo> participants = new HashMap<Player, PlayerInfo>();

	/**
	 * The number of players (not observers) present.
	 */
	private int playerCount = 0;

	/**
	 * The orders that have been submitted this turn.
	 */

	/**
	 * The state of this server
	 */
	private SERVER_STATE state = SERVER_STATE.WAITING_FOR_PLAYERS;

	// private List<OrderSpec> submittedOrders = new ArrayList<OrderSpec>();
	/**
	 * The information for the current turn. This is put into
	 * <code>turnHistory</code> as soon as the current turn is over.
	 */
	private TurnInfo thisTurn;

	/**
	 * The listing of all information for the turns that have passed.
	 */

	private List<TurnInfo> turns = new LinkedList<TurnInfo>();

	/**
	 * A list of powers that have not yet been assigned.
	 */
	private List<StandardToken> unassignedPowers;

	/**
	 * The information for the current game.
	 */
	private VariantInfo vInfo;

	/**
	 * Holds the length of the deadline for the current time.
	 */
	private int currentDeadlineLimit = -1;

	/**
	 * Holds the length of time that has passed so far this phase.
	 */
	@SuppressWarnings("unused")
	private int currentTurnTimeElapsed;
	//TODO remove this suppress warning.
	

	/**
	 * The number of players who have submitted orders this turn.
	 */
	private int submitedCount = 0;

	/**
	 * The historian this server uses.
	 */
	// TODO Upgrade this to a ServerHistorian
	private TokenHistorian historian = new TokenHistorian();

	/**
	 * The mapping this server uses.
	 */
	private TokenMap myMapping;

	public final void connected() {

	}

	public final void disconnected() {

	}

	/**
	 * Gets the information (supply centre location, army position and order
	 * resolution) for the current turn.
	 * 
	 * @return The information for the current turn
	 * @throws IllegalServerStateException
	 *         If the game had not started yet or was paused.
	 * @see com.danicsoft.daide.serverclient.Server#getCurrentTurnInfo()
	 */
	public TurnInfo getCurrentTurnInfo() throws IllegalServerStateException {
		if (state == SERVER_STATE.GAME_ENDED || state == SERVER_STATE.WAITING_FOR_PLAYERS) {
			throw new IllegalServerStateException(this);
		}
		return thisTurn;
	}

	/**
	 * Gets the map information for this game.
	 * 
	 * @return A n object representing the map information.
	 */
	public MapInfo getMapInfo() throws IllegalServerStateException {
		if(state == SERVER_STATE.GAME_ENDED) {
			throw new IllegalServerStateException(this);
		}
		MapInfo mInfo = new MapInfo();
		mInfo.defintion = mapDefinition();
		mInfo.name = mapName();
		return mInfo;
	}

	/**
	 * Gets the number of powers we must have to start a game
	 * 
	 * @return The number of powers we require.
	 */
	private int getMaxPowers() {
		return maxPowers == 0 ? maxPowers = powers().size() : maxPowers;

	}

	/**
	 * Gets the current state of this server
	 * 
	 * @return The state of this server
	 */
	public SERVER_STATE getState() {
		return state;
	}

	/**
	 * Gets the variant information for this server.
	 * 
	 * @return The variant information for this game.
	 */
	public VariantInfo getVariantInfo() throws IllegalServerStateException {
		if (state == SERVER_STATE.WAITING_FOR_PLAYERS) {
			throw new IllegalServerStateException(this);
		}
		return vInfo == null ? vInfo = variantInfo() : vInfo;
	}

	public void left(Player leftPlayer) {

		if (state == SERVER_STATE.GAME_STARTED || state == SERVER_STATE.GAME_PAUSED) {
			playerCount--;
			notifyCivilDisorder(leftPlayer);
			PlayerInfo pInfo = participants.get(leftPlayer);
			if (pInfo != null) {
				pInfo.isReady = false;
			}
			if (vInfo.isDeadlineDisconnect()) {
				pauseGame();
			}
		}
		else {
			participants.remove(leftPlayer);

		}

	}

	public synchronized boolean join(Player player) {
		if (state != SERVER_STATE.WAITING_FOR_PLAYERS) {
			return false;
		}
		PlayerInfo newInfo = new PlayerInfo(player);
		newInfo.isObserver = false;
		participants.put(player, newInfo);
		try {
			player.notifyMapInfo(getMapInfo());
		}
		catch (IllegalServerStateException e) {
			
		}
		return true;
	}

	/**
	 * Begin listening on the specified port for incoming connections.
	 * 
	 * @param port
	 *        The port to listen on.
	 * @param map
	 *        The mapping to associate with this server.
	 * @throws IOException
	 *         If we could not connect, or lost our connection.
	 */
	public void listen(int port, TokenMap map) throws IOException {
		myMapping = map;
		ServerConnection.listen(port, this, historian);
	}

	public void newConnection(Connection conn) {

		try {
			new PlayerProxy(conn, new TokenParser(myMapping, conn, historian), this);
		}
		catch (IOException e) {
			conn.severConnection();
		}

	}

	public boolean rejoin(Player player) {
		if (state == SERVER_STATE.WAITING_FOR_PLAYERS || state == SERVER_STATE.GAME_ENDED)
			return false;

		PlayerInfo pInfo = participants.get(player);
		if (pInfo == null || pInfo.isConnected) {
			return false;
		}
		
		try {
			player.notifyMapInfo(getMapInfo());
		}
		catch (IllegalServerStateException e) {
			
		}

		pInfo.player = player;
		pInfo.isReady = true;
		playerCount++;
		notifyNotCivilDisorder(player);
		if (state == SERVER_STATE.GAME_PAUSED) {
			if (playerCount == getMaxPowers()) {
				unpauseGame();
			}
		}
		return true;

	}

	/**
	 * Inform the server that we have finished processing the map and are ready
	 * to join the game. Note that the game may start before this method
	 * returns.
	 * 
	 * @param player
	 *        The player that is ready
	 * @return True if we are allowed to play this game and we are already
	 *         joined
	 */
	public synchronized boolean ready(Player player) {
		PlayerInfo pInfo = participants.get(player);

		if (state != SERVER_STATE.WAITING_FOR_PLAYERS) {
			return false;
		}
		if (pInfo == null) {
			if (join(player)) {
				pInfo = participants.get(player);
			}
			else {
				return false;
			}
		}

		if (pInfo.isObserver) {
			return false;
		}

		pInfo.isReady = true;
		playerCount++;
		if (playerCount == getMaxPowers()) {
			doStartGame();
		}
		return true;

	}

	public boolean observe(Player player) {
		if(state == SERVER_STATE.GAME_ENDED) {
			return false;
		}
		PlayerInfo newInfo = new PlayerInfo(player);
		newInfo.isObserver = true;
		participants.put(player, newInfo);
		try {
			player.notifyMapInfo(getMapInfo());
		}
		catch (IllegalServerStateException e) {
			
		}
		return true;
	}

	/**
	 * Submit orders to the server. If orders are missing, the player will be
	 * notified as such.
	 * 
	 * @param orders
	 *        The orders to submit.
	 * @param power
	 *        The power these are the orders for.
	 * @throws IllegalServerStateException
	 *         If the game has not started, or has ended.
	 */
	public void submitOrders(List<OrderSpec> orders, Player power) throws IllegalServerStateException {

		if (state != SERVER_STATE.GAME_STARTED && state != SERVER_STATE.GAME_PAUSED) {
			throw new IllegalServerStateException(this);
		}
		PlayerInfo pInfo = participants.get(power);
		boolean hadAll = pInfo.hasAllOrders;
		for (OrderSpec order : orders) {
			byte response = validateOrder(order);
			if (response == OrderNotes.MBV || vInfo.isAnyOrdersAccepted()) {
				pInfo.submittedOrders.remove(order);
				pInfo.submittedOrders.add(order);
			}
			power.acceptOrder(order, response);
		}
		pInfo.lookForMissingOrders(true);
		if (currentDeadlineLimit < 0) {
			if (pInfo.hasAllOrders) {
				if (!hadAll) {
					submitedCount++;
				}
			}
			else {
				if (hadAll) {
					submitedCount--;
				}
			}
		}

		if (submitedCount == getMaxPowers()) {
			doNextTurn();
		}

	}

	/**
	 * Unsubmit the order specified.
	 * 
	 * @param orders
	 *        The orders to cancel.
	 * @param power
	 *        The power responsible for cancelling this order.
	 * @return True if the server was able to cancel the order, false otherwise.
	 * @see com.danicsoft.daide.serverclient.Server#unsubmitOrder(java.util.List,
	 *      com.danicsoft.daide.serverclient.Player)
	 */
	public boolean unsubmitOrder(List<OrderSpec> orders, Player power) {
		PlayerInfo pInfo = participants.get(power);
		boolean allGood = true;
		for(OrderSpec order : orders) {
			if (!pInfo.submittedOrders.remove(order)) {
				allGood = false;
			}
		}
		if(!allGood) {
			return false;
		}
		boolean hadAll = pInfo.hasAllOrders;
		pInfo.lookForMissingOrders(false);
		if (hadAll && !pInfo.hasAllOrders) {
			submitedCount--;
		}
		return true;

	}

	/**
	 * Cancel orders previously sent.
	 * 
	 * @param power
	 *        The power to cancel them for.
	 * @throws IllegalServerStateException
	 *         If the game has not started, or has ended.
	 * @see com.danicsoft.daide.serverclient.Server#unsubmitOrders(com.danicsoft.daide.serverclient.Player)
	 */
	public void unsubmitOrders(Player power) throws IllegalServerStateException {
		PlayerInfo pInfo = participants.get(power);
		pInfo.resetOrders();

	}

	/**
	 * Advances play to the next turn. It calls <code>nextTurn</code> and then
	 * updates its internal state.
	 */
	private void doNextTurn() {

		thisTurn = new TurnInfo();
		Set<OrderSpec> submittedOrders = new HashSet<OrderSpec>();
		for (PlayerInfo player : participants.values()) {
			if (!player.isObserver) {
				submittedOrders.addAll(player.submittedOrders);
				player.resetOrders();
				submitedCount--;
			}
		}
		
		turns.add(thisTurn);
		nextTurn();
		thisTurn.orders = processOrders(submittedOrders);
		thisTurn.armyPosition = armyPosition();
		thisTurn.scOwnership = supplyOwnership();
		thisTurn.turn = currentTurn();
		for (Player player : participants.keySet()) {
			player.nextTurn(thisTurn);
		}
	}

	/**
	 * Starts the game. Initializes all information required and calls
	 * <code>startGame()</code>.
	 */
	private void doStartGame() {
		if (state != SERVER_STATE.WAITING_FOR_PLAYERS) {
			return;
		}
		state = SERVER_STATE.GAME_STARTED;
		unassignedPowers = new ArrayList<StandardToken>(powers());
		thisTurn = new TurnInfo();
		thisTurn.armyPosition = armyPosition();
		thisTurn.scOwnership = supplyOwnership();
		thisTurn.turn = thisTurn.armyPosition.getTurn();
		for (PlayerInfo player : participants.values()) {

			player.startGame();

		}

		startGame();

	}

	/**
	 * Gets a randomly generated unassigned power token.
	 * 
	 * @return A randomly generated power token.
	 */
	private StandardToken getUnassignedPower() {

		if (unassignedPowers == null || unassignedPowers.size() == 0) {
			throw new NoSuchElementException("There were no more powers to distribute.");
		}
		int i = Math.abs(rand.nextInt()) % unassignedPowers.size();
		return unassignedPowers.remove(i);

	}

	/**
	 * Notify all players that a player has entered civil disorder.
	 * 
	 * @param player
	 *        The player that has gone into civil disorder.
	 */
	private void notifyCivilDisorder(Player player) {
		// TODO Automatically generated
		player.getVersion();

	}

	/**
	 * Notify all players that a player has left civil disorder.
	 * 
	 * @param player
	 *        The player that has left civil disorder.
	 */
	private void notifyNotCivilDisorder(Player player) {
		// TODO Automatically generated
		player.getVersion();

	}

	/**
	 * Pause the game. If this is a deadline game, suspend all deadlines and
	 * inform all players that the deadlines have been suspended.
	 */
	private void pauseGame() {
		state = SERVER_STATE.GAME_PAUSED;
		// TODO Add in timer handling code for deadlines.

	}

	/**
	 * Unpause the game. If this is a deadline game, resume all deadlines and
	 * inform all players that the deadlines have restarted.
	 */
	private void unpauseGame() {
		state = SERVER_STATE.GAME_STARTED;
		// TODO Add in timer handling code for deadlines.

	}

	/**
	 * Returns whether or not a given order is valid for the person giving it.
	 * 
	 * @param order
	 *        The order to validate.
	 * @return An integer representing the validity of this order. This is one
	 *         of the {@link TokenMap.OrderNotes} constants.
	 */
	private byte validateOrder(OrderSpec order) {
		// TODO add some validation in here you tool.
		order.hashCode();
		return OrderNotes.MBV;
	}

	/**
	 * Gets the army position for this turn
	 * 
	 * @return The army position.
	 */
	protected abstract NowCommand armyPosition();

	/**
	 * Gets the current turn.
	 * 
	 * @return The current turn.
	 */
	protected abstract Turn currentTurn();

	/**
	 * Subclasses must implement this command to give a map defintion for the
	 * current game
	 * 
	 * @return A MapDefinitionCommand for the current game.
	 */
	protected abstract MapDefinitionCommand mapDefinition();

	/**
	 * Gets the name of the map this server plays on.
	 * 
	 * @return A String based representation of the name of the map.
	 */
	protected abstract String mapName();

	/**
	 * Advances the game by one turn.
	 */
	protected abstract void nextTurn();

	/**
	 * Subclasses must implement this command to inform the server how many
	 * players are required to start playing, or 0 for infinite
	 * 
	 * @return How many players this game requires.
	 */
	protected abstract List<StandardToken> powers();

	/**
	 * Process the orders for a given turn.
	 * 
	 * @param orders
	 *        The orders to process.
	 * @return A list of the resulting <code>OrderCommand</code>s derived
	 *         from resolving the orders specified.
	 */
	protected abstract List<OrderCommand> processOrders(Set<OrderSpec> orders);

	/**
	 * Start the game. Begin processing and prepaing to recieve status queries.
	 */
	protected abstract void startGame();

	/**
	 * Gets the supply centre ownership for this turn.
	 * 
	 * @return The supply centre ownership.
	 */
	protected abstract SupplyOwnershipCommand supplyOwnership();

	/**
	 * Gets the variant information for this turn
	 * 
	 * @return The variant information for this turn.
	 */
	protected abstract VariantInfo variantInfo();
}
